package zd.vrp.global;

import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import zd.vrp.initsolution.SolomonParameter;

public class Route {

	private City firstCity;
	private City lastCity;
	private int routeSize;

	private int lastCityIndex;

	// route total capacity
	private double totalCapacity;
	private double totalLength;
	private List<City> cityList = new LinkedList<City>();

	// in order to improve efficiency, we use a hashmap to record history
	// arrivaltime value
	private Map<String, Double> arrivalTimeComputingHistory;

	public Route() {
		City depot = GlobalInformation.cityList.get(0);

		firstCity = new City(0);
		lastCity = new City(0);

		// inialization
		firstCity.setDemand(depot.getDemand());
		firstCity.setEtime(depot.getEtime());
		firstCity.setLtime(depot.getLtime());
		firstCity.setRoute(this);
		firstCity.setRoutePosition(0);
		firstCity.setServiceTime(depot.getServiceTime());
		firstCity.setX(depot.getX());
		firstCity.setY(depot.getY());
		firstCity.setArrivalTime(0);

		lastCity.setDemand(depot.getDemand());
		lastCity.setEtime(depot.getEtime());
		lastCity.setLtime(depot.getLtime());
		lastCity.setRoute(this);
		lastCity.setRoutePosition(0);
		lastCity.setServiceTime(depot.getServiceTime());
		lastCity.setX(depot.getX());
		lastCity.setY(depot.getY());
		lastCity.setArrivalTime(0);

		cityList.add(firstCity);
		cityList.add(lastCity);

		routeSize = 2;
		lastCityIndex = routeSize - 1;

		totalCapacity = 0;
		totalLength = 0;

		// initialize arrivalTimeComputingHistory
		arrivalTimeComputingHistory = new HashMap<String, Double>();
	}

	// calculate start time of inserted city
	public double arrivalTime4InsertedCity(City city, int position) {
		// first check history computing
		if (arrivalTimeComputingHistory.get(city.getId() + "_" + position) == null) {
			City previousCity = cityList.get(position - 1);
			double newArrivalTime = previousCity.getFinishTime()
					+ GlobalInformation.distance[previousCity.getId()
							* GlobalInformation.cityNum + city.getId()]
					/ GlobalInformation.vechielSpeed;
			arrivalTimeComputingHistory.put(city.getId() + "_" + position,
					newArrivalTime);
			return newArrivalTime;
		} else {
			double arrivalTime = arrivalTimeComputingHistory.get(city.getId()
					+ "_" + position);
			return arrivalTime;
		}
	}

	// get the start time of following city when a city is inserted
	public double getStartTimeOfFollowingCityAfterInsertion(City city,
			int position) {
		City followingCity = cityList.get(position);
		double uArrivalTime = arrivalTime4InsertedCity(city, position);
		double uStartTime = Math.max(uArrivalTime, city.getEtime());
		double jArrivalTime = uStartTime
				+ city.getServiceTime()
				+ GlobalInformation.distance[city.getId()
						* GlobalInformation.cityNum + followingCity.getId()];
		double jStartTime = Math.max(jArrivalTime, followingCity.getEtime());
		return jStartTime;
	}

	// insert City into this route
	public boolean insertCityIntoRoute(City city, int position) {
		//City followingCity = cityList.get(position);
		//0. add city into city list
		cityList.add(position, city);
		
		//1.update route size and unvisited city number
		setRouteSize(routeSize + 1);
		totalCapacity += city.getDemand();
		GlobalInformation.unvisitedCity--;
		
		//2.update city route information
		city.setRoute(this);
		city.setRoutePosition(position);
		
		//3. update route arrival information
		double arrivalTime = arrivalTime4InsertedCity(city, position);
		city.setArrivalTime(arrivalTime);
		
		//4. update downstream cities
		City previousCity = city;
		for(int i=position+1;i<cityList.size();i++){
			City followingCity = cityList.get(i);
			double arrivalTimeF = previousCity.getFinishTime() + GlobalInformation.getDistanceBetweenCities(previousCity, followingCity)/GlobalInformation.vechielSpeed;
			followingCity.setRoutePosition(followingCity.getRoutePosition());
			followingCity.setArrivalTime(arrivalTimeF);
			previousCity = followingCity;
		}

		arrivalTimeComputingHistory.clear();

		return true;
	}
	
	public boolean removeCityFromRoute(int position) {
		//City followingCity = cityList.get(position);
		//0. add city into city list
		City removeCity = cityList.get(position);
		cityList.remove(position);
		
		//1.update route size and unvisited city number
		setRouteSize(routeSize - 1);
		totalCapacity = totalCapacity - removeCity.getDemand();
		GlobalInformation.unvisitedCity++;
		
		//2.update city route information
		removeCity.setRoute(null);
		removeCity.setRoutePosition(-1);
		
		//3. update route arrival information, update downstream cities
		City previousCity = cityList.get(position - 1);
		for(int i=position;i<cityList.size();i++){
			City followingCity = cityList.get(i);
			double arrivalTimeF = previousCity.getFinishTime() + GlobalInformation.getDistanceBetweenCities(previousCity, followingCity)/GlobalInformation.vechielSpeed;
			followingCity.setRoutePosition(followingCity.getRoutePosition());
			followingCity.setArrivalTime(arrivalTimeF);
			previousCity = followingCity;
		}

		arrivalTimeComputingHistory.clear();

		return true;
	}
	
	// get insertion cost
	public double getInsertionDistanceCost(City city, int position) {
		City previousCity = cityList.get(position - 1);
		City followingCity = cityList.get(position);
		double distanceCost = GlobalInformation.getDistanceBetweenCities(
				previousCity, city)
				+ GlobalInformation.getDistanceBetweenCities(city,
						followingCity)
				- GlobalInformation.getDistanceBetweenCities(previousCity,
						followingCity);

		return distanceCost;
	}

	// get insertion distance cost for solomon insertion type I
	public double getInsertionDistanceCostSolomonI(City city, int position) {
		City previousCity = cityList.get(position - 1);
		City followingCity = cityList.get(position);
		double distanceCost = GlobalInformation.getDistanceBetweenCities(
				previousCity, city)
				+ GlobalInformation.getDistanceBetweenCities(city,
						followingCity)
				- SolomonParameter.iu
				* GlobalInformation.getDistanceBetweenCities(previousCity,
						followingCity);

		return distanceCost;
	}

	// check demand feasibility of inserting a city
	public boolean checkDemandFeasibility(City city) {
		if (totalCapacity + city.getDemand() > GlobalInformation.vehicleCapacity) {
			return false;
		}
		return true;
	}

	// check time window feasibility
	public boolean checkTimeFeasibility(City city, int position) {

		City followingCity = cityList.get(position);

		double arrivalTimeU = arrivalTime4InsertedCity(city, position);
		double startTimeU = Math.max(city.getEtime(), arrivalTimeU);

		if (startTimeU > city.getLtime()) {
			return false;
		}


		double arrivalTimeJ = startTimeU
				+ city.getServiceTime()
				+ GlobalInformation.getDistanceBetweenCities(city,
						followingCity) / GlobalInformation.vechielSpeed;

		double startTimeJ = Math.max(arrivalTimeJ, followingCity.getEtime());


		if (startTimeJ > followingCity.getLtime()) {
			return false;
		}

		double pushForwardJ = startTimeJ - followingCity.getStartTime();
		
	
		for (int i = position + 1; i < cityList.size(); i++) {
			City tCity = cityList.get(i);

			pushForwardJ = Math.max(0, pushForwardJ - tCity.getWaitingTime());
			
			if (pushForwardJ + tCity.getStartTime() > tCity.getLtime()) {
				return false;
			}
		}

		return true;
	}

	// update route time information
	public void updateRouteTime() {

	}

	public City getFirstCity() {
		return firstCity;
	}

	public void setFirstCity(City firstCity) {
		this.firstCity = firstCity;
	}

	public City getLastCity() {
		return lastCity;
	}

	public void setLastCity(City lastCity) {
		this.lastCity = lastCity;
	}

	public int getRouteSize() {
		return routeSize;
	}

	public void setRouteSize(int routeSize) {
		this.routeSize = routeSize;
		this.lastCityIndex = routeSize - 1;
	}

	public int getLastCityIndex() {
		return lastCityIndex;
	}

	public void setLastCityIndex(int lastCityIndex) {
		this.lastCityIndex = lastCityIndex;
	}

	public List<City> getCityList() {
		return cityList;
	}

	public void setCityList(List<City> cityList) {
		this.cityList = cityList;
	}
	
	
}
